require 'base64'
require 'iconv'

HEX = 16;
UTF8 = 2;
ASCII = 3;
BASE64 = 4;

#
#
#
class ByteString
  include Enumerable  
  attr_accessor :encoding, :bs, :length
  #
  #
  #
  def initialize(str,encoding)
    @encoding = encoding;
    if str.kind_of?(Array) 
      @bs = str
    else  
      case @encoding
        when HEX
          @bs = HexString.parse_hex_string(str);
        when ASCII
          @bs = HexString.parse_ascii_string(str);
        when BASE64
          @bs = HexString.parse_base64_string(str);
        when UTF8
          @bs = HexString.parse_utf8_string(str);
      end     
    end  
    @length = @bs.length
  end
  
  #
  #
  #
  def each &block  
    @bs.each{|byte| block.call( byte )}  
  end 


  #
  #
  #
  def getBytes 
    return @bs
  end
  #
  #
  #
  def byteAt(offset)
    raise(ArgumentError, "Offset for byteAt() out of range", caller) if offset<0 or offset>=@length
    return @bs[offset]&0xFF
  end
  
  #
  #
  #
  def bytes(offset, count=nil)
    return ByteString.new('',HEX) if offset<0 or offset>=@length
    count = @length - offset unless count
    count = @length - offset if (count<0) or (offset+count>@length)
    return ByteString.new(@bs[offset,count],HEX)
  end
  
  #
  #
  #
  def xor bs_data
    return ByteString.new( @bs.zip(bs_data.getBytes).map { |a,b| (a^b) }, @encoding)
  end
  
  #
  #
  #
  def getL format
      size = TLV.get_length_field_size_helper @length,format
      len_bytes = TLV.length_to_bytes @length,size,format
      return ByteString.new(len_bytes,HEX)
  end  
  
  #
  #
  #
  def getLV format
      return getL(format).concat(self)
  end 
  #
  #
  #
  def not
    return ByteString.new(@bs.map{ |x| (0xFF^x) },@encoding)
  end
  
  def or bs_data
    return ByteString.new(@bs.zip(bs_data.getBytes).map{ |a,b| (a|b) }, @encoding)
  end
  
  def and bs_data
    return ByteString.new(@bs.zip(bs_data.getBytes).map{ |a,b| (a&b) }, @encoding)
  end
  #
  #
  #
  def left(count)
    count = @length if (count>@length) or (count<0)
    return ByteString.new(@bs[0,count],HEX)
  end
  
  #
  #
  #
  def right(count)
    count = @length if (count>@length) or (count<0)
    return ByteString.new(@bs[@length-count,count],HEX)    
  end
  
  #
  #
  #
  def concat(bs)
    temp=[] << @bs << bs.getBytes
    temp.flatten!
    return ByteString.new(temp,HEX)
  end
  #
  #
  #
  def find(search,offset=nil)
    raise(ArgumentError, "First argument for find() must be ByteString",caller) unless search.kind_of?(ByteString)
    raise(ArgumentError, "Second argument for find() must be ByteString",caller) if (offset) and (!offset.kind_of?(Fixnum)) 
    len = search.length
    @bs.each_cons(len).with_index do |cons, index|
      if(offset)
        return index if (cons == search.getBytes) and (index >= offset)
      else
        return index if cons == search.getBytes
      end
    end
    nil
  end
  
  #
  #
  #
  def toUnsigned littleEndian=false
    bytes = littleEndian ? @bs.reverse : @bs
    bytes.inject( ) { |r, n| r << 8 | n }         
  end
  
  #
  #
  #
  def toSigned littleEndian=false
    bytes = littleEndian ? @bs.reverse : @bs
    sign_byte = littleEndian ? ("%c" %@bs[@bs.length - 1]) : ("%c" %@bs[0])
    if ((@bs.length > 0) && (sign_byte.unpack('c')[0] < 0))
      bytes.inject(-1) { |r, n| r << 8 | n }
    else
      bytes.inject( ) { |r, n| r << 8 | n }  
    end                        
  end
  #
  #
  #
  def == (other_bs)
    return true if other_bs.equal?(self)
    return false unless other_bs.kind_of?(self.class)
    @bs == other_bs.getBytes()
  end
  
  #
  #
  #
  def toString(encoding=nil)
    response = nil;
    if(!encoding) then
      response = HexString.hexifyByteArray(@bs);
    else
      case encoding
        when HEX then
          response = HexString.hexifyByteArray(@bs);
        when ASCII then
          response = HexString.bytesToString(@bs);
        when BASE64 then
          response = HexString.bytesToBase64(@bs);
        when UTF8 then
          response = HexString.bytesToUTF8String(@bs);
      end
    end
    return response;
  end
  
  def to_s
    toString()
  end
end

#
#
#
class HexString
  def self.parse_hex_string(str)
    str = str.gsub(/^(0x)?/,"");
    str = str.gsub(/[^A-Fa-f0-9]/,"");
    return str.scan(/../).collect {|x| x.hex};
  end 
  
  def self.parse_ascii_string(str)
    return str.bytes.to_a
  end
  
  def self.parse_base64_string(str)
    return Base64.decode64(str).bytes.to_a
  end
  
  def self.parse_utf8_string(str)
    result = []
    str.scan(/./mu).each{ |c| 
      c.each_byte{|x| result << x} 
    }
    return result;
  end
  
  def self.hexifyByteArray(bytes)
    bytes.collect{|x| "%02x" %x }.join("")
  end
  
  def self.bytesToString(bytes)
    bytes.pack('c*') 
  end
  
  def self.bytesToUTF8String(bytes)
    bytes.pack("c*") 
  end
  
  #In ruby-1.9.2 you have Base64.strict_encode64 which 
  #doesn't add that \n (newline) at the end
  def self.bytesToBase64(bytes)
    Base64.encode64(bytesToString(bytes)).gsub("\n", '')
  end
  
  #
  #Hex dump with optinal formatter hash
  #
  def self.dump bytes, formatter={}
    width = formatter[:width] || 16 
    case_sensitive  = formatter[:case] || :lower
    annotate = formatter[:annotate] || :ascii
    prefix = formatter[:prefix] || ""
    numbering = formatter[:numbering] || :hex_bytes
    format = formatter[:format] || 2
    indent    = formatter[:indent] || 0
    
    1.upto(indent){ prefix += " "}
    str = ""
    0.step(bytes.length-1, width) {|i|
      string = bytes[i,width] 

      hex = string.unpack("H*")[0]
      hex.upcase! if case_sensitive == :upper
      
      hex.gsub!(/(.{#{format}})/) {|m|
        m+" "
      }
      
      string.gsub!(/[\000-\037]/, ".")

      len = (2*width/format)*(format+1)
      str << prefix
      str << "%07X: " % (i) if numbering == :hex_bytes 
      str << ("%-#{len}s" % hex)
      str << " #{string}" if annotate == :ascii
      str << "\n" 
      
    }
    str << "\n"
    puts str   
  end
end